/*
 * MegaMek - Copyright (C) 2001,2002,2003,2004 Ben Mazur (bmazur@sev.org)
 *
 *  This program is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License as published by the Free
 *  Software Foundation; either version 2 of the License, or (at your option)
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 *  or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 *  for more details.
 */

/*
 * ClubAttackAction.java
 *
 * Created on April 3, 2002, 2:37 PM
 */

package megamek.common.actions;

import megamek.common.BipedMech;
import megamek.common.Compute;
import megamek.common.CriticalSlot;
import megamek.common.Entity;
import megamek.common.GunEmplacement;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.ILocationExposureStatus;
import megamek.common.Mech;
import megamek.common.MiscType;
import megamek.common.Mounted;
import megamek.common.TargetRoll;
import megamek.common.Targetable;
import megamek.common.ToHitData;
import megamek.common.VTOL;

/**
 * The attacker makes a club attack on the target. This also covers mech melee
 * weapons like hatchets.
 * 
 * @author Ben
 * @version
 */
public class ClubAttackAction extends PhysicalAttackAction {

	/** The Constant serialVersionUID. */
	private static final long serialVersionUID = -8744665286254604559L;
	
	/** The club. */
	private Mounted club;
	
	/** The aiming. */
	private int aiming;

	/**
	 * Creates new ClubAttackAction.
	 *
	 * @param entityId the entity id
	 * @param targetId the target id
	 * @param club the club
	 * @param aimTable the aim table
	 */
	public ClubAttackAction(int entityId, int targetId, Mounted club,
			int aimTable) {
		super(entityId, targetId);
		this.club = club;
		aiming = aimTable;
	}

	/**
	 * Instantiates a new club attack action.
	 *
	 * @param entityId the entity id
	 * @param targetType the target type
	 * @param targetId the target id
	 * @param club the club
	 * @param aimTable the aim table
	 */
	public ClubAttackAction(int entityId, int targetType, int targetId,
			Mounted club, int aimTable) {
		super(entityId, targetType, targetId);
		this.club = club;
		aiming = aimTable;
	}

	/**
	 * Damage that the specified mech does with a club attack.
	 *
	 * @param entity the entity
	 * @param club the club
	 * @param targetInfantry the target infantry
	 * @return the damage for
	 */
	public static int getDamageFor(Entity entity, Mounted club,
			boolean targetInfantry) {
		MiscType mType = (MiscType) (club.getType());
		int nDamage = (int) Math.floor(entity.getWeight() / 5.0);
		if (mType.hasSubType(MiscType.S_SWORD)) {
			nDamage = (int) (Math.ceil(entity.getWeight() / 10.0) + 1.0);
		} else if (mType.hasSubType(MiscType.S_MACE_THB)) {
			nDamage *= 2;
		} else if (mType.hasSubType(MiscType.S_RETRACTABLE_BLADE)) {
			nDamage = (int) Math.ceil(entity.getWeight() / 10.0);
		} else if (mType.hasSubType(MiscType.S_MACE)) {
			nDamage = (int) Math.floor(entity.getWeight() / 4.0);
		} else if (mType.hasSubType(MiscType.S_PILE_DRIVER)) {
			// Pile Drivers have constant damage, not variable like most.
			nDamage = 10;
		} else if (mType.hasSubType(MiscType.S_FLAIL)) {
			// Flails have constant damage, not variable like most.
			nDamage = 9;
		} else if (mType.hasSubType(MiscType.S_DUAL_SAW)) {
			// Saws have constant damage, not variable like most.
			nDamage = 7;
		} else if (mType.hasSubType(MiscType.S_CHAINSAW)) {
			// Saws have constant damage, not variable like most.
			nDamage = 5;
		} else if (mType.hasSubType(MiscType.S_BACKHOE)) {
			// Backhoes have constant damage, not variable like most.
			nDamage = 6;
		} else if (mType.isShield()) {
			nDamage = club.getDamageAbsorption(entity, club.getLocation());
		} else if (mType.hasSubType(MiscType.S_WRECKING_BALL)) {
			// Wrecking Balls have constant damage, not variable like most.
			nDamage = 8;
		} else if (mType.hasSubType(MiscType.S_BUZZSAW)) {
			// buzzsaw does 2d6 damage, not weight dependant
			nDamage = Compute.d6(2);
		} else if (mType.isVibroblade()) {
			if (club.curMode().equals("Active")) {
				if (mType.hasSubType(MiscType.S_VIBRO_LARGE)) {
					nDamage = 14;
				} else if (mType.hasSubType(MiscType.S_VIBRO_MEDIUM)) {
					nDamage = 10;
				} else {
					nDamage = 7;
				}
			} else {
				// when not active a vibro blade does normal sword damage
				nDamage = (int) (Math.ceil(entity.getWeight() / 10.0) + 1.0);
			}
		} else if (mType.hasSubType(MiscType.S_CHAIN_WHIP)) {
			nDamage = 3;
		} else if (mType.hasSubType(MiscType.S_ROCK_CUTTER)) {
			nDamage = 5;
		}

		// TSM doesn't apply to some weapons, including Saws.
		if (entity.heat >= 9
				&& !(mType.hasSubType(MiscType.S_DUAL_SAW)
						|| mType.hasSubType(MiscType.S_CHAINSAW)
						|| mType.hasSubType(MiscType.S_PILE_DRIVER)
						|| mType.isShield()
						|| mType.hasSubType(MiscType.S_WRECKING_BALL)
						|| mType.hasSubType(MiscType.S_FLAIL)
						|| (mType.isVibroblade() && club.curMode().equals(
								"Active"))
						|| mType.hasSubType(MiscType.S_BUZZSAW) || mType
							.hasSubType(MiscType.S_CHAIN_WHIP))
				&& ((Mech) entity).hasTSM()) {
			nDamage *= 2;
		}
		int clubLocation = club.getLocation();
		// tree clubs don't have a location--use right arm (is this okay?)
		if (clubLocation == Entity.LOC_NONE) {
			clubLocation = Mech.LOC_RARM;
		}
		if (entity.getLocationStatus(clubLocation) == ILocationExposureStatus.WET) {
			nDamage /= 2.0f;
		}
		if (targetInfantry) {
			nDamage = Math.max(1, nDamage / 10);
		}

		return nDamage
				+ entity.getCrew().modifyPhysicalDamagaForMeleeSpecialist();
	}

	/**
	 * To hit.
	 *
	 * @param game the game
	 * @return the to hit data
	 */
	public ToHitData toHit(IGame game) {
		return ClubAttackAction.toHit(game, getEntityId(),
				game.getTarget(getTargetType(), getTargetId()), getClub(),
				aiming);
	}

	/**
	 * To-hit number for the specified club to hit.
	 *
	 * @param game the game
	 * @param attackerId the attacker id
	 * @param target the target
	 * @param club the club
	 * @param aimTable the aim table
	 * @return the to hit data
	 */
	public static ToHitData toHit(IGame game, int attackerId,
			Targetable target, Mounted club, int aimTable) {
		final Entity attackEntity = game.getEntity(attackerId);

		// arguments legal?
		checkForIllegalArguments(target, club, attackEntity);

		String impossible = PhysicalAttackAction.toHitIsImpossible(game, attackEntity,
				target);
		if (impossible != null) {
			return new ToHitData(TargetRoll.IMPOSSIBLE, impossible);
		}

		// non-mechs can't club
		if (!(attackEntity instanceof Mech)) {
			return new ToHitData(TargetRoll.IMPOSSIBLE, "Non-mechs can't club");
		}

		// Quads can't club
		if (attackEntity.entityIsQuad()) {
			return new ToHitData(TargetRoll.IMPOSSIBLE, "Attacker is a quad");
		}

		if (((MiscType) club.getType())
				.hasSubType(MiscType.S_RETRACTABLE_BLADE)
				&& !((Mech) attackEntity).hasExtendedRetractableBlade()) {
			return new ToHitData(TargetRoll.IMPOSSIBLE, "Blade is Retracted.");
		}

		if (attackEntity.getGrappled() != Entity.NONE
				&& attackEntity.getGrappleSide() == Entity.GRAPPLE_LEFT
				&& club.getLocation() == Mech.LOC_LARM) {
			return new ToHitData(TargetRoll.IMPOSSIBLE, "impossible");
		}

		if (attackEntity.getGrappled() != Entity.NONE
				&& attackEntity.getGrappleSide() == Entity.GRAPPLE_RIGHT
				&& club.getLocation() == Mech.LOC_RARM) {
			return new ToHitData(TargetRoll.IMPOSSIBLE, "impossible");
		}

		IHex attHex = game.getBoard().getHex(attackEntity.getPosition());
		IHex targHex = game.getBoard().getHex(target.getPosition());
		final int attackerElevation = attackEntity.getElevation() + attHex.getElevation();
		final int attackerHeight = attackerElevation + attackEntity.height();
		final int targetElevation = target.getElevation()
				+ targHex.getElevation();
		final int targetHeight = targetElevation + target.getHeight();
		final boolean bothArms = (club.getType().hasFlag(MiscType.F_CLUB) && ((MiscType) club
				.getType()).hasSubType(MiscType.S_CLUB));
		final boolean hasClaws = (((BipedMech) attackEntity).hasClaw(Mech.LOC_RARM) || ((BipedMech) attackEntity)
				.hasClaw(Mech.LOC_LARM));
		final boolean shield = ((MiscType) club.getType()).isShield();
		boolean needsHand = handIsNeeded(club, hasClaws);

		ToHitData toHit;

		if (bothArms) {
			// check if both arms are present & operational
			if (attackEntity.isLocationBad(Mech.LOC_RARM)
					|| attackEntity.isLocationBad(Mech.LOC_LARM)) {
				return new ToHitData(TargetRoll.IMPOSSIBLE, "Arm missing");
			}
			// check if attacker has fired arm-mounted weapons
			if (attackEntity.weaponFiredFrom(Mech.LOC_RARM)
					|| attackEntity.weaponFiredFrom(Mech.LOC_LARM)) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Weapons fired from arm this turn");
			}
			// need shoulder and hand actuators
			if (!attackEntity.hasWorkingSystem(Mech.ACTUATOR_SHOULDER, Mech.LOC_RARM)
					|| !attackEntity.hasWorkingSystem(Mech.ACTUATOR_SHOULDER,
							Mech.LOC_LARM)) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Shoulder actuator destroyed");
			}
			if ((!attackEntity.hasWorkingSystem(Mech.ACTUATOR_HAND, Mech.LOC_RARM) || !attackEntity
					.hasWorkingSystem(Mech.ACTUATOR_HAND, Mech.LOC_LARM))
					&& needsHand) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Hand actuator destroyed");
			}
		} else if (shield) {
			if (!attackEntity.hasPassiveShield(club.getLocation())) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Shield not in passive mode");
			}
		} else if (((MiscType) club.getType()).hasSubType(MiscType.S_FLAIL)) {
			if (!attackEntity.hasWorkingSystem(Mech.ACTUATOR_UPPER_ARM,
					club.getLocation())) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Upper actuator destroyed");
			}
			if (!attackEntity.hasWorkingSystem(Mech.ACTUATOR_LOWER_ARM,
					club.getLocation())) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Lower actuator destroyed");
			}
		} else {
			// check if arm is present
			if (attackEntity.isLocationBad(club.getLocation())) {
				return new ToHitData(TargetRoll.IMPOSSIBLE, "Arm missing");
			}
			// check if attacker has fired arm-mounted weapons
			if (attackEntity.weaponFiredFrom(club.getLocation())) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Weapons fired from arm this turn");
			}
			// need shoulder and hand actuators
			if (!attackEntity.hasWorkingSystem(Mech.ACTUATOR_SHOULDER, club.getLocation())) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Shoulder actuator destroyed");
			}
			if (!attackEntity.hasWorkingSystem(Mech.ACTUATOR_HAND, club.getLocation())
					&& needsHand) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Hand actuator destroyed");
			}
		}

		// club must not be damaged
		if (!shield
				&& attackEntity.getBadCriticals(CriticalSlot.TYPE_EQUIPMENT,
						attackEntity.getEquipmentNum(club), club.getLocation()) > 0) {
			return new ToHitData(TargetRoll.IMPOSSIBLE, "Club is damaged");
		}

		// check elevation (target must be within one level, except for VTOL)
		if (target instanceof VTOL && ((VTOL) target).isFlying()) {
			if (targetElevation - attackerElevation > 3
					|| targetElevation - attackerElevation < 0) {
				return new ToHitData(TargetRoll.IMPOSSIBLE,
						"Target elevation not in range");
			}
		} else if (targetHeight < attackerElevation
				|| targetElevation > attackerHeight) {
			return new ToHitData(TargetRoll.IMPOSSIBLE,
					"Target elevation not in range");
		}

		// check facing
		int clubArc = bothArms ? Compute.ARC_FORWARD
				: (club.getLocation() == Mech.LOC_LARM ? Compute.ARC_LEFTARM
						: Compute.ARC_RIGHTARM);
		if (!Compute.isInArc(attackEntity.getPosition(), attackEntity.getSecondaryFacing(),
				target.getPosition(), clubArc)) {
			return new ToHitData(TargetRoll.IMPOSSIBLE, "Target not in arc");
		}

		// can't club while prone
		if (attackEntity.isProne()) {
			return new ToHitData(TargetRoll.IMPOSSIBLE, "Attacker is prone");
		}

		// Attacks against adjacent buildings automatically hit.
		if (target.getTargetType() == Targetable.TYPE_BUILDING
				|| target.getTargetType() == Targetable.TYPE_FUEL_TANK
				|| target instanceof GunEmplacement) {
			return new ToHitData(TargetRoll.AUTOMATIC_SUCCESS,
					"Targeting adjacent building.");
		}

		// Set the base BTH
		int base = attackEntity.getCrew().getPiloting();

		// Various versions of physical weapons have different base bonuses and
		// penalties.
		base = setBaseBonus(club, base);

		toHit = new ToHitData(base, "base");

		PhysicalAttackAction.setCommonModifiers(toHit, game, attackEntity, target);

		// damaged or missing actuators
		if (bothArms) {
			addBothArmsModifierToHit(attackEntity, hasClaws, toHit);
		} else {
			if (!attackEntity.hasWorkingSystem(Mech.ACTUATOR_UPPER_ARM,
					club.getLocation())) {
				toHit.addModifier(2, "Upper arm actuator destroyed");
				if ((((MiscType) club.getType()).hasSubType(MiscType.S_LANCE))) {
					return new ToHitData(TargetRoll.IMPOSSIBLE,
							"Unable to use lance with upper arm actuator missing or destroyed");
				}
			}
			if (!attackEntity.hasWorkingSystem(Mech.ACTUATOR_LOWER_ARM,
					club.getLocation())) {
				toHit.addModifier(2, "Lower arm actuator missing or destroyed");
				if ((((MiscType) club.getType()).hasSubType(MiscType.S_LANCE))) {
					return new ToHitData(TargetRoll.IMPOSSIBLE,
							"Unable to use lance with lower arm actuator missing or destroyed");
				}
			}
			// Rules state +2 bth if your using a club with claws.
			if (hasClaws) {
				toHit.addModifier(2, "Mek has claws");
			}
			if (attackEntity.hasFunctionalArmAES(club.getLocation())) {
				toHit.addModifier(-1, "AES modifer");
			}
		}

		// elevation
		setHitTableBasedOnElevation(target, aimTable, attackerElevation,
				targetElevation, shield, toHit);

		// factor in target side
		toHit.setSideTable(Compute.targetSideTable(attackEntity, target));

		// done!
		return toHit;
	}

	/**
	 * Check for illegal arguments.
	 *
	 * @param target the target
	 * @param club the club
	 * @param ae the ae
	 */
	private static void checkForIllegalArguments(Targetable target,
			Mounted club, final Entity ae) {
		if (ae == null || target == null) {
			throw new IllegalArgumentException("Attacker or target not valid");
		}
		if (club == null) {
			throw new IllegalArgumentException("Club is null");
		}
		if (club.getType() == null) {
			throw new IllegalArgumentException("Club type is null");
		}
	}

	/**
	 * Sets the hit table based on elevation.
	 *
	 * @param target the target
	 * @param aimTable the aim table
	 * @param attackerElevation the attacker elevation
	 * @param targetElevation the target elevation
	 * @param shield the shield
	 * @param toHit the to hit
	 */
	private static void setHitTableBasedOnElevation(Targetable target,
			int aimTable, final int attackerElevation,
			final int targetElevation, final boolean shield, ToHitData toHit) {
		if (attackerElevation == targetElevation) {
			if (shield) {
				toHit.setHitTable(ToHitData.HIT_PUNCH);
			} else {
				toHit.setHitTable(aimTable);
				if (aimTable != ToHitData.HIT_NORMAL) {
					toHit.addModifier(4, "called shot");
				}
			}
		} else if (attackerElevation < targetElevation) {
			if (target.getHeight() == 0) {
				if (shield) {
					toHit.setHitTable(ToHitData.HIT_PUNCH);
				} else {
					toHit.setHitTable(ToHitData.HIT_NORMAL);
				}
			} else {
				toHit.setHitTable(ToHitData.HIT_KICK);
			}
		} else {
			toHit.setHitTable(ToHitData.HIT_PUNCH);
		}
	}

	/**
	 * Adds the both arms modifier to hit.
	 *
	 * @param ae the ae
	 * @param hasClaws the has claws
	 * @param toHit the to hit
	 */
	private static void addBothArmsModifierToHit(final Entity ae,
			final boolean hasClaws, ToHitData toHit) {
		if (!ae.hasWorkingSystem(Mech.ACTUATOR_UPPER_ARM, Mech.LOC_RARM)) {
			toHit.addModifier(2, "Upper arm actuator destroyed");
		}
		if (!ae.hasWorkingSystem(Mech.ACTUATOR_UPPER_ARM, Mech.LOC_LARM)) {
			toHit.addModifier(2, "Upper arm actuator destroyed");
		}
		if (!ae.hasWorkingSystem(Mech.ACTUATOR_LOWER_ARM, Mech.LOC_RARM)) {
			toHit.addModifier(2, "Lower arm actuator missing or destroyed");
		}
		if (!ae.hasWorkingSystem(Mech.ACTUATOR_LOWER_ARM, Mech.LOC_LARM)) {
			toHit.addModifier(2, "Lower arm actuator missing or destroyed");
		}
		if (hasClaws) {
			toHit.addModifier(2, "Mek has claws");
		}
		if (ae.hasFunctionalArmAES(Mech.LOC_RARM)
				&& ae.hasFunctionalArmAES(Mech.LOC_LARM)) {
			toHit.addModifier(-1, "AES modifer");
		}
	}

	/**
	 * Sets the base bonus.
	 *
	 * @param club the club
	 * @param base the base
	 * @return the int
	 */
	private static int setBaseBonus(Mounted club, int base) {
		if (((MiscType) club.getType()).hasSubType(MiscType.S_PILE_DRIVER)) {
			base += 2;
		} else if (((MiscType) club.getType()).hasSubType(MiscType.S_BACKHOE)
				|| ((MiscType) club.getType())
						.hasSubType(MiscType.S_ROCK_CUTTER)
				|| ((MiscType) club.getType())
						.hasSubType(MiscType.S_WRECKING_BALL)
				|| ((MiscType) club.getType()).hasSubType(MiscType.S_LANCE)
				|| ((MiscType) club.getType()).hasSubType(MiscType.S_FLAIL)
				|| ((MiscType) club.getType()).hasSubType(MiscType.S_MACE)
				|| ((MiscType) club.getType()).hasSubType(MiscType.S_MACE_THB)) {
			base += 1;
		} else if (((MiscType) club.getType()).hasSubType(MiscType.S_CHAINSAW)
				|| ((MiscType) club.getType()).hasSubType(MiscType.S_DUAL_SAW)) {
			base += 0;
		} else if (((MiscType) club.getType()).hasSubType(MiscType.S_HATCHET)
				|| ((MiscType) club.getType())
						.hasSubType(MiscType.S_MINING_DRILL)) {
			base -= 1;
		} else if (((MiscType) club.getType()).hasSubType(MiscType.S_COMBINE)
				|| ((MiscType) club.getType())
						.hasSubType(MiscType.S_RETRACTABLE_BLADE)
				|| ((MiscType) club.getType()).hasSubType(MiscType.S_SWORD)
				|| ((MiscType) club.getType())
						.hasSubType(MiscType.S_CHAIN_WHIP)
				|| ((MiscType) club.getType())
						.hasSubType(MiscType.S_SHIELD_SMALL)
				|| ((MiscType) club.getType()).isVibroblade()) {
			base -= 2;
		} else if (((MiscType) club.getType())
				.hasSubType(MiscType.S_SHIELD_MEDIUM)) {
			base -= 3;
		} else if (((MiscType) club.getType())
				.hasSubType(MiscType.S_SHIELD_LARGE)) {
			base -= 4;
		} else {
			base -= 1;
		}
		return base;
	}

	/**
	 * Hand is needed.
	 *
	 * @param club the club
	 * @param hasClaws the has claws
	 * @return true, if successful
	 */
	private static boolean handIsNeeded(Mounted club, final boolean hasClaws) {
		boolean needsHand = true;

		if (hasClaws
				|| (((MiscType) club.getType()).hasSubType(MiscType.S_FLAIL))
				|| (((MiscType) club.getType())
						.hasSubType(MiscType.S_WRECKING_BALL))
				|| (((MiscType) club.getType()).hasSubType(MiscType.S_LANCE))
				|| (((MiscType) club.getType()).hasSubType(MiscType.S_BUZZSAW))
				|| (((MiscType) club.getType()).hasSubType(MiscType.S_DUAL_SAW))) {
			needsHand = false;
		}
		return needsHand;
	}

	/**
	 * Gets the club.
	 *
	 * @return the club
	 */
	public Mounted getClub() {
		return club;
	}

	/**
	 * Sets the club.
	 *
	 * @param club the new club
	 */
	public void setClub(Mounted club) {
		this.club = club;
	}
}
